﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace MedievalGameProject
{
    public class Terrain : DrawableGameComponent
    {
        int vertexCountX;
        int vertexCountZ;
        float blockScale;
        float heightScale;
        byte[] heightmap;
        int numVertices;
        int numTriangles;
        VertexBuffer vb;
        IndexBuffer ib;
        BasicEffect effect;
        Texture2D texture;

        float halfTerrainWidth;
        float halfTerrainDepth;
        float lowestTerrainHeight;
        float highestTerrainHeight;
        Vector2 StartPosition;

        public Terrain(Game game)
            : base(game)
        {

        }

        public override void Initialize()
        {
            base.Initialize();

            lowestTerrainHeight = 0;
            highestTerrainHeight = 0;
        }

        public void Load(string heightmapFileName, int vertexCountX, int vertexCountZ,
            float blockScale, float heightScale)
        {
            Initialize();

            effect = new BasicEffect(GraphicsDevice);
            texture = Game.Content.Load<Texture2D>("terraintexture");

            this.vertexCountX = vertexCountX;
            this.vertexCountZ = vertexCountZ;
            this.blockScale = blockScale;
            this.heightScale = heightScale;

            FileStream filestream = File.OpenRead(Game.Content.RootDirectory + "/" + heightmapFileName + ".raw");

            heightmap = new byte[vertexCountX * vertexCountZ];

            for (int i = 0; i < vertexCountZ; i++)
            {
                for (int j = 0; j < vertexCountX; j++)
                {
                    int heightVal = filestream.ReadByte();
                    float worldHeightVal = heightVal * heightScale;

                    highestTerrainHeight = Math.Max(worldHeightVal, highestTerrainHeight);
                    lowestTerrainHeight = Math.Min(worldHeightVal, lowestTerrainHeight);

                    heightmap[j + vertexCountX * +i] = (byte)heightVal;
                }
                //throw away byte at end of row for reach mode to work
                filestream.ReadByte();
            }

            filestream.Close();

            GenerateTerrainMesh();
        }

        private void GenerateTerrainMesh()
        {
            numVertices = vertexCountX * vertexCountZ;
            numTriangles = (vertexCountX - 1) * (vertexCountZ - 1) * 2;

            short[] indices = GenerateTerrainIndices();
            VertexPositionTexture[] vertices = GenerateTerrainVertices(indices);

            vb = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), numVertices, BufferUsage.WriteOnly);
            vb.SetData<VertexPositionTexture>(vertices);

            ib = new IndexBuffer(GraphicsDevice, typeof(short), numTriangles * 3, BufferUsage.WriteOnly);
            ib.SetData<short>(indices);

        }

        private short[] GenerateTerrainIndices()
        {
            int numIndices = numTriangles * 3;
            short[] indices = new short[numIndices];

            int indicesCount = 0;
            for (int i = 0; i < (vertexCountZ - 1); i++) //All Rows except last
            {
                for (int j = 0; j < (vertexCountX - 1); j++) //All Columns except last
                {
                    int index = j + i * vertexCountZ; //2D coordinates to linear
                    //First Triangle Vertices
                    indices[indicesCount++] = (short) index;
                    indices[indicesCount++] = (short) (index + 1);
                    indices[indicesCount++] = (short) (index + vertexCountX + 1);

                    //Second Triangle Vertices
                    indices[indicesCount++] = (short) (index + vertexCountX + 1);
                    indices[indicesCount++] = (short) (index + vertexCountX);
                    indices[indicesCount++] = (short) index;
                }
            }
            return indices;
        }

        private VertexPositionTexture[] GenerateTerrainVertices(short[] terrainIndices)
        {
            halfTerrainWidth = (vertexCountX - 1) * blockScale * .5f;
            halfTerrainDepth = (vertexCountZ - 1) * blockScale * .5f;
            StartPosition = new Vector2(-halfTerrainWidth, -halfTerrainDepth);

            float tuDerivative = 1.0f / (vertexCountX - 1);
            float tvDerivative = 1.0f / (vertexCountZ - 1);

            VertexPositionTexture[] vertices = new VertexPositionTexture[vertexCountX * vertexCountZ];

            int vertexCount = 0;
            float tu = 0;
            float tv = 0;

            for (float i = -halfTerrainDepth; i <= halfTerrainDepth; i += blockScale)
            {
                tu = 0.0f;
                for (float j = -halfTerrainWidth; j <= halfTerrainWidth; j += blockScale)
                {
                    vertices[vertexCount].Position = new Vector3(j, heightmap[vertexCount] * heightScale, i);
                    vertices[vertexCount].TextureCoordinate = new Vector2(tu, tv);

                    tu += tuDerivative;
                    vertexCount++;
                }
                tv += tvDerivative;
            }

            return vertices;
        }

        public override void Draw(GameTime gameTime)
        {
            if (Game1.currentGameState == GameState.InGame)
            {
                effect.World = Matrix.Identity; //No transformation of the terrain
                effect.View = Game1.camera.view;
                effect.Projection = Game1.camera.projection;
                effect.Texture = texture;
                effect.TextureEnabled = true;

                GraphicsDevice.SetVertexBuffer(vb); //Set vertices
                GraphicsDevice.Indices = ib; //Set indices

                foreach (EffectPass CurrentPass in effect.CurrentTechnique.Passes)
                {
                    CurrentPass.Apply();
                    int firstCallNumTriangles = numTriangles / 2;
                    int firstCallNumVertices = firstCallNumTriangles * 3;
                    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, firstCallNumVertices, 0, firstCallNumTriangles);
                    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, Math.Abs(numVertices - firstCallNumVertices),
                        firstCallNumVertices, numTriangles - firstCallNumTriangles);
                }


                base.Draw(gameTime);
            }
        }

        public float GetHeight(Vector3 pos)
        {
            return GetHeight(pos.X, pos.Z);
        }

        public float GetHeight(Vector2 xzLoc)
        {
            return GetHeight(xzLoc.X, xzLoc.Y);
        }

        public float GetHeight(float x, float z)
        {
            float height = -999999;
            Vector2 positionInGrid = new Vector2(x - StartPosition.X, z - StartPosition.Y);
            Vector2 blockPosition = new Vector2(positionInGrid.X / blockScale, positionInGrid.Y / blockScale);
            if (blockPosition.X >= 0 && blockPosition.X < (vertexCountX - 1) &&
                blockPosition.Y >= 0 && blockPosition.Y < (vertexCountZ - 1))
            {
                Vector2 blockOffset = new Vector2(blockPosition.X - (int)blockPosition.X, blockPosition.Y - (int)blockPosition.Y);
                int vertexIndex = (int)blockPosition.X + (int)blockPosition.Y * vertexCountX;
                float height1 = heightmap[vertexIndex + 1];
                float height2 = heightmap[vertexIndex];
                float height3 = heightmap[vertexIndex + vertexCountX + 1];
                float height4 = heightmap[vertexIndex + vertexCountX];
                float heightIncX, heightIncY;
                //Top triangle
                if (blockOffset.X > blockOffset.Y)
                {
                    heightIncX = height1 - height2;
                    heightIncY = height3 - height1;
                }
                //Bottom triangle
                else
                {
                    heightIncX = height3 - height4;
                    heightIncY = height4 - height2;
                }

                float lerpHeight = height2 + heightIncX * blockOffset.X + heightIncY * blockOffset.Y;
                height = lerpHeight * heightScale;
            }

            return height;
        }

        public float? Intersects(Ray ray)
        {
            //This won't be changed if the Ray doesn't collide with terrain            
            float? collisionDistance = null;
            //Size of step is half of blockScale
            Vector3 rayStep = ray.Direction * blockScale * 0.5f;
            //Need to save start position to find total distance once collision point is found
            Vector3 rayStartPosition = ray.Position;

            Vector3 lastRayPosition = ray.Position;
            ray.Position += rayStep;
            float height = GetHeight(ray.Position);
            while (ray.Position.Y > height && height >= lowestTerrainHeight)
            {
                lastRayPosition = ray.Position;
                ray.Position += rayStep;
                height = GetHeight(ray.Position);
            }

            if (height >= lowestTerrainHeight) //Lowest possible point of terrain
            {
                Vector3 startPosition = lastRayPosition;
                Vector3 endPosition = ray.Position;
                // Binary search. Find the exact collision point
                for (int i = 0; i < 32; i++)
                {
                    // Binary search pass
                    Vector3 middlePoint = (startPosition + endPosition) * 0.5f;
                    if (middlePoint.Y < height)
                        endPosition = middlePoint;
                    else
                        startPosition = middlePoint;
                }
                Vector3 collisionPoint = (startPosition + endPosition) * 0.5f;
                collisionDistance = Vector3.Distance(rayStartPosition, collisionPoint);
            }//end if


            return collisionDistance;
        }
    }
}